package com.zh.service.impl;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zh.common.ErrorCode;
import com.zh.config.MinioConfig;
import com.zh.domain.Model.UpLoadChunkTmp;
import com.zh.domain.dto.file.*;
import com.zh.domain.entity.SysFile;
import com.zh.exception.ThrowUtils;
import com.zh.mapper.SysFileMapper;
import com.zh.service.FileService;
import com.zh.utils.MinioUtil;
import com.zh.utils.RedisUtil;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class FileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> implements FileService {
    private static final String MD5_PREFIX = "jiyan:md5:";
    private static final String TMP_PREFIX = "tmp";
    // 允许的文件格式集合
    Set<String> allowedSuffixes = new HashSet<>(Arrays.asList(
            // 视频格式
            "mp4", "avi", "mkv", "mov", "flv", "wmv", "mpg", "mpeg", "m4v", "webm",
            "3gp", "ts", "m2ts", "mts", "vob", "ogv", "rmvb", "asf", "amv", "divx",
            "xvid", "m2v", "qt", "dat", "rm", "svi", "viv", "nuv", "nsv", "f4v", "f4p",
            "f4a", "f4b", "m2v", "m4p", "mkv", "mov", "qt", "vob", "webm", "ogv", "rm", "rmvb", "asf", "amv", "divx", "xvid", "3gp", "ts", "m2ts", "mts", "f4v", "f4p", "f4a", "f4b",

            // 音频格式
            "flac", "aac", "wav", "mp3", "ogg", "wma", "aiff", "ape", "mid", "midi",
            "ra", "ram", "rpm", "smf", "wavpack", "wv", "tta", "ac3", "dts", "dsd", "dsf", "opus", "mpc", "m4a", "wv", "flac", "ape", "tak", "wavpack", "dsd", "dsf",

            // 图片格式
            "jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp", "svg", "ico", "raw",
            "cr2", "nef", "orf", "sr2", "arw", "dng", "heif", "heic", "avif", "jfif",
            "psd", "ai", "eps", "indd", "cdr", "tga", "ppm", "pgm", "pbm", "pnm", "hdr",
            "exr", "crw", "mrw", "kdc", "dcr", "x3f", "mos", "orf", "pef", "rw2", "sr2",
            "srf", "arw", "raf", "heif", "heic", "avif", "jfif", "psd", "ai", "eps", "indd", "cdr", "tga", "ppm", "pgm", "pbm", "pnm", "hdr", "exr", "crw", "mrw", "kdc", "dcr", "x3f", "mos", "orf", "pef", "rw2", "sr2", "srf", "arw", "raf",

            // 文档格式
            "doc", "docx", "odt", "rtf", "txt", "pdf", "ppt", "pptx", "xls", "xlsx", "ods",

            // 压缩格式
            "zip", "rar", "tar", "gz", "bz2", "7z",

            // 数据库格式
            "sql", "db", "sqlite", "csv", "json", "xml",

            // 代码格式
            "java", "py", "js", "ts", "html", "css", "scss", "less", "sass", "json", "xml", "md", "txt", "php", "ruby", "perl", "go", "swift", "kotlin", "scala", "rust", "dart", "typescript", "vue", "react", "angular", "node", "bash", "sh", "bat", "ps1", "sql", "graphql", "yaml", "yml", "dockerfile", "makefile", "gradle", "maven", "ant", "cmake", "nginx", "apache", "tomcat", "docker", "kubernetes", "helm", "terraform", "ansible", "jenkinsfile", "gitignore", "gitattributes", "editorconfig",

            // 其他格式
            "exe", "dll", "jar", "war", "ear", "apk", "ipa", "dmg", "iso", "bin", "dat", "log", "ini", "cfg", "conf", "properties", "env", "lock", "bak", "tmp", "swp", "bak", "old", "new", "bak", "tmp", "log", "csv", "tsv", "xlsx", "xls", "pptx", "ppt", "docx", "doc", "pdf", "rtf", "odt", "ods", "odp", "odg", "odf", "odc", "odb", "odi", "odf", "odm", "ott", "ots", "otp", "otg", "otf", "otc", "otm", "oth", "ots", "otp", "otg", "otf", "otc", "otm", "oth"
    ));
    private final MinioConfig minioConfig;
    private final RedisUtil redisUtil;
    private final MinioUtil minioUtil;
    private final SysFileMapper sysFileMapper;
    private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();

    public FileServiceImpl(MinioConfig minioConfig, RedisUtil redisUtil, MinioUtil minioUtil, SysFileMapper sysFileMapper) {
        this.minioConfig = minioConfig;
        this.redisUtil = redisUtil;
        this.minioUtil = minioUtil;
        this.sysFileMapper = sysFileMapper;
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public String saveFile(UploadFileDto uploadFileDto) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        int loginId = StpUtil.getLoginIdAsInt();
        String md5 = DigestUtil.md5Hex(uploadFileDto.getFile().getInputStream());
        String existsUrl = checkFileExist(md5);
        if (StrUtil.isNotEmpty(existsUrl)) {
            return existsUrl;
        }
        String suffix = getSuffix(uploadFileDto.getFileName());
        ThrowUtils.throwIf(!checkSuffix(suffix), ErrorCode.PARAMS_ERROR, "不允许的文件格式");
        minioUtil.createBucket(minioConfig.getBucketName());
        String fileName = suffix + uploadFileDto.getFileName().split("\\.")[0]
                + System.currentTimeMillis() + "." + suffix;
        UpLoadChunkTmp upLoadChunkTmp = UpLoadChunkTmp.builder()
                .bucketName(minioConfig.getBucketName())
                .objectName(fileName)
                .stream(uploadFileDto.getFile().getInputStream())
                .chunkSize(uploadFileDto.getFile().getSize())
                .build();
        minioUtil.upLoadChunk(upLoadChunkTmp);
        String url = minioUtil.getUrl(minioConfig.getBucketName(), fileName);
        // 存储数据库
        long size = uploadFileDto.getFile().getSize();
        SysFile sysFile = SysFile.builder()
                .url(url)
                .fileName(uploadFileDto.getFileName())
                .fileSize((double) size)
                .md5(md5)
                .type(suffix)
                .build();
        sysFile.setStatus(1);
        sysFile.setCreateBy(loginId);
        sysFile.setCreateAt(LocalDateTime.now());
        sysFile.setUpdateBy(loginId);
        sysFile.setUpdateAt(LocalDateTime.now());
        sysFileMapper.insert(sysFile);
        //存缓存
        String fileKey = MD5_PREFIX + md5;
        Map<String, Object> map = MapUtil.createMap(HashMap.class);
        map.put("url", url);
        map.put("fileSize", size);
        redisUtil.setHash(fileKey, map, 31, TimeUnit.DAYS);
        log.info("文件上传成功，fileName:{},url：{}", uploadFileDto.getFileName(), url);
        return url;
    }

    @Override
    @Transactional
    public String deleteFileByUrl(DeleteFileDto deleteFileDto) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        String url = deleteFileDto.getUrl();
        ThrowUtils.throwIf(StrUtil.isEmpty(url), ErrorCode.PARAMS_ERROR, "url不能为空");
        SysFile sysFile = BeanUtil.copyProperties(deleteFileDto, SysFile.class);
        LambdaUpdateWrapper<SysFile> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(SysFile::getUrl, sysFile.getUrl());
        boolean success = sysFileMapper.delete(updateWrapper) > 0;
        if (success) {
            //删除文件
            String objectName = url.split(minioConfig.getBucketName())[1].substring(1);
            minioUtil.deleteObject(minioConfig.getBucketName(), objectName);
            return "删除成功";
        }
        return "操作错误";
    }

    @Override
    @Transactional
    public String saveChunk(UploadChunkDto uploadChunkDto) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        //1.redis是否存在这个文件Md5或分片md5，存在直接返回分片md5代表已上传，不存在在进行以下步骤
        if (checkChunk(uploadChunkDto.getFileMd5()) || checkChunk(uploadChunkDto.getChunkMd5())) {
            return uploadChunkDto.getChunkMd5();
        }
        //2.上传分片
        String suffix = getSuffix(uploadChunkDto.getFileName());
        minioUtil.createBucket(minioConfig.getBucketName());
        UpLoadChunkTmp upLoadChunkTmp = UpLoadChunkTmp.builder()
                .bucketName(minioConfig.getBucketName())
                .objectName(TMP_PREFIX + "/" + uploadChunkDto.getFileMd5() + "." + suffix + "/" + uploadChunkDto.getChunkMd5() + "." + suffix)
                .chunkName(uploadChunkDto.getChunkMd5())
                .chunkSize(uploadChunkDto.getChunk().getSize())
                .stream(uploadChunkDto.getChunk().getInputStream())
                .build();
        minioUtil.upLoadChunk(upLoadChunkTmp);
        //3.上传分片后保存到redis
        String chunkKey = MD5_PREFIX + uploadChunkDto.getChunkMd5();
        Map<String, Object> map = MapUtil.createMap(HashMap.class);
        map.put("fileName", uploadChunkDto.getFileName());
        map.put("fileMd5", uploadChunkDto.getFileMd5());
        map.put("chunkMd5", uploadChunkDto.getChunkMd5());
        redisUtil.setHash(chunkKey, map, 31, TimeUnit.DAYS);
        log.info("上传分片成功，chunkMd5:{},fileName: {}", uploadChunkDto.getChunkMd5(), uploadChunkDto.getFileName());
        return uploadChunkDto.getChunkMd5();
    }

    @Override
    @Transactional
    public String mergeChunk(MergeChunkDto mergeChunkDto) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        //1.检查redis或mysql是否存在该md5的url，存在直接返回url
        if (ObjectUtil.isNotEmpty(checkFileExist(mergeChunkDto.getFileMd5()))) {
            return checkFileExist(mergeChunkDto.getFileMd5());
        }
        Iterable<Result<Item>> listedObjects = minioUtil
                .getClient()
                .listObjects(ListObjectsArgs.builder()
                        .bucket(minioConfig.getBucketName())
                        .prefix(TMP_PREFIX + "/" + mergeChunkDto.getFileMd5() + "." + getSuffix(mergeChunkDto.getFileName()) + "/")
                        .build());
        //遍历所有分片 计算总大小
        double fileSize = 0.0;
        List<ComposeSource> sourceObjectList = new ArrayList<>();
        for (Result<Item> object : listedObjects) {
            Item item = object.get();
            fileSize += item.size();
            sourceObjectList.add(ComposeSource.builder()
                    .bucket(minioConfig.getBucketName())
                    .object(item.objectName()).build());
        }
        ThrowUtils.throwIf(fileSize == 0.0 || sourceObjectList.isEmpty(), ErrorCode.PARAMS_ERROR, "文件上传异常！");
        //对分片进行排序
        sourceObjectList.sort(
                (o1, o2) -> {
                    String o1Name = o1.object();
                    String o2Name = o2.object();
                    Integer o1Index = Integer.parseInt(o1Name.split("-")[1].split("\\.")[0]);
                    Integer o2Index = Integer.parseInt(o2Name.split("-")[1].split("\\.")[0]);
                    return o1Index - o2Index;
                }
        );
        //文件夹名->文件类型
        String suffix = getSuffix(mergeChunkDto.getFileName());
        // 调用 composeObject 方法合并分片
        String fileName = suffix + "/" + mergeChunkDto.getFileName().split("\\.")[0]
                + System.currentTimeMillis() + "." + suffix;
        minioUtil.composeChunk(minioConfig.getBucketName(), fileName, sourceObjectList);
        //获取合并后的对象地址
        String url = minioUtil.getUrl(minioConfig.getBucketName(), fileName);
        // 存储数据库
        SysFile sysFile = SysFile.builder()
                .url(url)
                .fileName(mergeChunkDto.getFileName())
                .fileSize(fileSize)
                .md5(mergeChunkDto.getFileMd5())
                .type(suffix)
                .build();
        Integer createBy = mergeChunkDto.getCreateBy();
        sysFile.setCreateBy(createBy);
        sysFile.setUpdateBy(createBy);
        sysFileMapper.insert(sysFile);
        String fileKey = MD5_PREFIX + mergeChunkDto.getFileMd5();
        Map<String, Object> map = MapUtil.createMap(HashMap.class);
        map.put("url", url);
        map.put("fileSize", fileSize);
        redisUtil.setHash(fileKey, map, 31, TimeUnit.DAYS);
        //删除分片文件
        EXECUTOR.execute(() -> {
            List<DeleteObject> deleteObjects = new ArrayList<>();
            for (Result<Item> object : listedObjects) {
                Item item;
                try {
                    item = object.get();
                } catch (ErrorResponseException | XmlParserException | ServerException | NoSuchAlgorithmException |
                         IOException | InvalidResponseException | InvalidKeyException | InternalException |
                         InsufficientDataException e) {
                    throw new RuntimeException(e);
                }
                deleteObjects.add(new DeleteObject(item.objectName()));
            }
            minioUtil.deleteChunk(minioConfig.getBucketName(), deleteObjects, fileName);
            redisUtil.deleteByPrefix(MD5_PREFIX + mergeChunkDto.getFileMd5() + "-");
        });
        log.info("合并分片成功，fileName:{},url:{}", mergeChunkDto.getFileName(), url);
        return url;
    }

    @Override
    public Boolean checkChunk(String chunkMd5) {
        String chunkKey = MD5_PREFIX + chunkMd5;
        if (redisUtil.exists(chunkKey)) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }

    @Override
    public String checkFileExist(String fileMd5) {
        //1. 校验文件是否已上传 秒传 前端接收到原文件的md5代表已存储，无需再传，但是需要判断名称不同，不存在在进行以下步骤
        //1.1.redis是否存在这个文件的md5，存在直接返回 url
        String fileKey = MD5_PREFIX + fileMd5;
        if (redisUtil.exists(fileKey)) {
            Map<String, Object> hashEntries = redisUtil.getHashEntries(fileKey);
            String url = (String) hashEntries.get("url");
            redisUtil.setHash(fileKey, hashEntries, 31, TimeUnit.DAYS);
            return url;
        } else {
            //1.2.数据库是否存在这个文件的md5，存在直接返回url
            LambdaQueryWrapper<SysFile> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(SysFile::getMd5, fileMd5);
            SysFile sysFile = sysFileMapper.selectOne(queryWrapper);
            if (ObjectUtil.isNotNull(sysFile) && ObjectUtil.isNotEmpty(sysFile)) {
                Map<String, Object> map = MapUtil.createMap(HashMap.class);
                map.put("url", sysFile.getUrl());
                map.put("fileSize", sysFile.getFileSize());
                sysFile.setUpdateAt(LocalDateTime.now());
                sysFile.setUpdateBy(StpUtil.getLoginIdAsInt());
                sysFileMapper.updateById(sysFile);
                redisUtil.setHash(fileKey, map, 31, TimeUnit.DAYS);
                return sysFile.getUrl();
            }
        }
        return null;
    }

    @Override
    public String saveBase64Image(UploadBase64Dto uploadBase64Dto) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        String base64Str = uploadBase64Dto.getBase64Str();
        ThrowUtils.throwIf(!base64Str.startsWith("data:image/") && base64Str.contains(","), ErrorCode.PARAMS_ERROR, "错误的base64编码！");
        String base64Data = base64Str.substring(base64Str.indexOf(",") + 1);
        byte[] data = Base64.decode(base64Data);
        //1.检查redis或mysql是否存在该md5的url，存在直接返回url
        String md5 = DigestUtil.md5Hex(data);
        if (ObjectUtil.isNotEmpty(checkFileExist(md5))) {
            return checkFileExist(md5);
        }
        String originalName = UUID.randomUUID().toString() + System.currentTimeMillis() + ".png";
        String suffix = getSuffix(originalName);
        String fileName = suffix + "/" + originalName;
        InputStream inputStream = new ByteArrayInputStream(data);
        UpLoadChunkTmp upLoadChunkTmp = UpLoadChunkTmp.builder()
                .bucketName(minioConfig.getBucketName())
                .objectName(fileName)
                .stream(inputStream)
                .chunkSize((long) data.length)
                .build();
        minioUtil.upLoadChunk(upLoadChunkTmp);
        String url = minioUtil.getUrl(minioConfig.getBucketName(), fileName);
        // 存储数据库
        SysFile sysFile = SysFile.builder()
                .url(url)
                .fileName(originalName)
                .fileSize((double) data.length)
                .md5(md5)
                .type(suffix)
                .build();
        sysFile.setCreateBy(StpUtil.getLoginIdAsInt());
        sysFile.setUpdateBy(StpUtil.getLoginIdAsInt());
        sysFileMapper.insert(sysFile);
        String fileKey = MD5_PREFIX + md5;
        Map<String, Object> map = MapUtil.createMap(HashMap.class);
        map.put("url", url);
        map.put("fileSize", data.length);
        redisUtil.setHash(fileKey, map, 31, TimeUnit.DAYS);
        return url;
    }


    private String getSuffix(String fileName) {
        try {
            return fileName.substring(fileName.lastIndexOf(".") + 1);
        } catch (StringIndexOutOfBoundsException e) {
            return "";
        }
    }

    private boolean checkSuffix(String suffix) {
        // 视频格式校验
        return allowedSuffixes.contains(suffix);
    }
}
